Node & Next.js 성능 최적화 가이드 10개 | 매거진에 참여하세요

인사이트/로그개발 관련
작성일 : 25.04.17

Node & Next.js 성능 최적화 가이드 10개

#next.js #react #node #express #성능 #개선 #팁 #이미지 #서버통신 #최적화

👉 본문을 50%이상을 읽으면 '여기까지다' 퀘스트가 완료됩니다(로그인 필수)

1. 집계 함수 최적화 – 무조건 실시간 호출은 금물

DB에서 COUNT, AVG, SUM 같은 집계 함수를 자주 실시간 호출하면, 특히 대용량 테이블에서는 쿼리 비용이 상당히 큽니다.

굳이 실시간 집계가 필요하지 않고, 오차가 나도 상관없다면, 실시간 호출하지말고 별도로 분리하는게 좋더라구요

처음에는 속도차이가 안나는데, 점점 느려지는 경우가 있습니다. 그게 다 집계 함수때문이라고 보입니다.

갑자기 느려졌다라고 판단이 되면, SUM, AVERAGE, COUNT 등 집계함수를 쓰는 호출을 점검해보세요

개선 전략:

  • - 요청 시마다 호출하지 않고 스케줄러(Cron)나 Worker를 통해 미리 계산

  • - 결과를 Redis나 DB에 저장하고 캐시하여 반환

  • - 필요 시 수동으로 새로 고침 버튼 제공

  • - 실시간 값은 상세화면에서만 제공하고, 바깥의 화면에서는 미리 저장된 값 사용

2. next/image 무조건 빠르지 않다

Next.js의 next/image는 최적화를 위해 내부적으로 이미지 리사이징 서버를 사용합니다.
하지만 오히려 간단한 static 이미지에는 불필요한 오버헤드가 될 수 있어요.

특히 서버에서 한번 거치고 가기 때문에, 생각보다 좀 느린 느낌에, 이거 처리하느라고 서버의 응답이 전체적으로 느려집니다.

저는 next/image 썻다가 그냥 걷어내니까 훨씬 빨라졌습니다.

next/image의 경우, build시 포함되는 이미지에만 사용하는게 맞는것 같습니다.

외부 호스팅을 next/image로 불러오는건 속도가 더 느려집니다.

추천 기준:

  • - CDN에 호스팅된 정적 이미지 → <img> 사용

  • - 웹서버내 업로드 이미지 → next/image 사용 + loader 최적화

  • - 서버리스 환경(FaaS)에서 SSR이면 next/image 피하기

대안:
<img loading="lazy"> + 크기 최적화 + CDN 활용 (S3 + CloudFront 등)

3. useInView – 스크롤 시점에서 불러오기

클라이언트 렌더링에서 불필요하게 모든 데이터를 초기에 로딩하지 말고,
화면에 진입하는 순간 불러오는 방식으로 네트워크 부담을 줄일 수 있습니다.

저는 useEffect가 그런 효과를 가지고 있는줄 알았습니다.

그게 아니라 useEffect는 유저가 보는것과 상관없이 그냥 component가 로딩되는 즉시 뭔가를 하기 때문에

유저가 보든 안보든 그냥 서버를 호출해버립니다.

rootMargin 같은 것을 활용하면, 보기 전에 미리 호출도 가능하니 적절히 사용하면 좋습니다.

대표 라이브러리: react-intersection-observer

const { ref, inView } = useInView(); 
useEffect(() => { 
if (inView) 
fetchData(); }, 
[inView]);
const { ref, inView } = useInView({
    threshold: 0.01, // 100%가 보일 때
    triggerOnce: true, // 한 번만 호출
    rootMargin: '400px',
  });

4. 무한 스크롤 (Infinite Scroll) 적용

사용자가 데이터를 더 보고자 할때 데이터를 불러오세요.

초기 로딩이 훨씬 가벼워집니다. 페이지네이션보다 UX가 좋고, 성능에도 이점이 있습니다.

다만 메모리 누수 방지, 중복 요청 방지 로직 필요합니다.

useRef같은것으로 , 실제 로딩하는지 여부를 저장해놓고 쓰는것이 효과적입니다.

그러면 중복 호출이 안되서 좋습니다.

아니면 throttle을 사용하거나, 둘중에 하나는 사용하는게 좋아보이네요

useEffect(() => { 
const onScroll = () => {
 if (scrollPosition + window.innerHeight >= document.body.scrollHeight) { loadMore(); } 
}; 

window.addEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll); }, []);

5. dynamic import – 필요한 시점에만 컴포넌트 로드

Next.js에서 SSR을 하지 않아도 되는 컴포넌트는 클라이언트에서만 동적으로 로드하게 만들어야 초기 로딩 속도가 빨라집니다.

이게 언제쓰냐면

굳이 처음부터 호출하지 않아도 되는, 가장 하단에 있는 컴포넌트의 경우 SSR로 불러오지 않고

다른것들 먼저 불러온다음에 나중에 호출하게 되면, 초기 로딩 속도가 올라갑니다.

아니면 , 뭔가 윈도우가 로드된다음에 무조건 호출되어야 하는것들에도 사용하면 좋습니다.

const BoothInfoComponent = dynamic(
  () => import('@components/dynamicComponents/boothDetail/boothInfoComponent'),
  { ssr: false }
);

6. Express 미들웨어 최소화 – 라우트별 필요한 것만

모든 요청에 공통적으로 적용되는 미들웨어가 많아지면, 처리 시간이 쌓이게 됩니다.

이게 설정이 복잡하면 복잡할 수록 뭔가 빌드시간이나 로딩시간이 오래 걸리는데요

설정을 보시고, 미들웨어를 삭제하시는 것이 좋습니다.

개선 전략:

  • 글로벌 미들웨어 최소화

  • 인증, 로깅은 필요한 라우터에만 적용

  • 단순한 요청에 대해선 빠른 반환 (예: robots.txt, ping)

app.use('/api/secure', authMiddleware); // 전체가 아닌 일부에만

7. 이미지는 webp로

이미지 png도 이제 크다는 말이 나오는것 같습니다.

이미지는 webp로 저장하면 왠만하면 다 호환이 되니까요, webp로 저장하게 끔 하시는것이 좋습니다.

특히 유저분들이 올리는 이미지의 경우 , 뭐를 올리시든지, webp로 변환해서 호출하는것이 좋은데요

sharp를 쓰면, 리사이즈도 되지만, 포맷도 webp로 바꿀수있습니다.

사이즈와 포맷을 둘다 바꿔주면 , 사이즈가 엄청나게 줄어드니까요 자동화 해놓으시면 좋습니다.

이렇게 해놓고 S3에 올리시고 호출하시면, 경량화가 됩니다.

const sharp = require('sharp');

const imageData = Buffer.from(response.data, 'binary');
sharp(imageData)
              .resize({ width: 800 }) // 원하는 크기로 리사이징
              .toFormat('webp')
              .toBuffer()
              .then((resizedImageData) => {

8. HTTP/2, CDN, 캐싱 정책 적극 활용

HTTP2 통신이, HTTP 일반 통신보다 빠르다고 하네요

대역폭도 좋다고 하구요. 그래서 HTTP2 적용하려는데, S3는 HTTP2를 지원하지 않습니다.

대신 cloudFront를 적용하면 된다고 하네요

적용하면 기존 S3의 URL을 d1s1r4xme959x6.cloudfront.net 바꿔주면 된다고 하니 한번만 적용하시면 되고요

저는 이렇게 함수만들어서 , 그냥 보이는 족족 변환해서 호출하고 있습니다.

function replaceCloudFrontURL(url) {
  if (url?.includes('https://letspl.s3.ap-northeast-2.amazonaws.com')) {
    return url.replace(
      'https://letspl.s3.ap-northeast-2.amazonaws.com',
      'https://d1s1r4xme959x6.cloudfront.net'
    );
  } else {
    return url;
  }
}

9. DB 커넥션 풀 관리 – 너무 많아도, 너무 적어도 문제

Express에서 DB 커넥션을 매번 생성하거나, 너무 큰 커넥션 풀을 설정하면 서버 메모리를 과도하게 차지하거나 연결 타임아웃이 발생합니다.

연결할때 idle 상태시 해제하는 값들을 넣어주면 좋다고 하네요

const pool = new Pool({ max: 10, idleTimeoutMillis: 30000 });

10. 비동기 처리 철저 – await 남발하지 말고 한번에 처리하기

Node.js에서 동기적으로 await을 연속적으로 사용하면 I/O 대기 시간이 누적됩니다.

예시 (잘못된 방식):

await saveToDB(); await sendEmail();

개선:

await Promise.all([ saveToDB(), sendEmail(), ]);

또한 forEach 내 async/await은 병렬 처리를 막으므로 map + Promise.all()을 사용해야 합니다.